/*
 * vim:set spell spl=en fo=wan1croql tw=80 ts=2 sw=2 sts=2 sta et ai cin ff=unix:
 *
 * Copyright (C) 2008, 2009 Mihai Şucan
 *
 * This file is part of libmacrame.
 *
 * Libmacrame is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Libmacrame is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Libmacrame.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $URL: http://code.google.com/p/libmacrame $
 * $Date: 2009-04-07 14:57:32 +0300 $
 *
 */

// Opera compatibility
if (!window.console) {
  /**
   * @namespace Holds a simple method used for debugging. Opera doesn't have the 
   * window.console API like Firefox+Firebug has.
   */
  window.console = {};
}

if (!window.console.log) {
  /**
   * Display a message in the debugger. If available, opera.postError is used, 
   * otherwise no message is displayed.
   *
   * @param {mixed} mixed Any number of arguments, each one is displayed in the 
   * debugger.
   */
  window.console.log = function () {
    var msg = '';

    for (var i = 0, n = arguments.length; i < n; i++) {
      msg += arguments[i] + ' ';
    }

    if (window.opera && window.opera.postError) {
      opera.postError(msg);
    } else {
      $t.debug(msg);
    }
  };
}

if (!window.console.warn) {
  /**
   * Display a message in the debugger. If available, opera.postError is used, 
   * otherwise no warning is displayed.
   *
   * @param {mixed} mixed Any number of arguments, each one is displayed in the 
   * debugger.
   */
  window.console.warn = function () {
    console.log.apply(null, arguments);
  };
}

/**
 * @class Holds the common methods and properties used for running tests.
 */
var $t = new (function () {
/*
 * This holds cached references to DOM elements.
 */
var elems = {};

/*
 * This holds the ID of the currently running test.
 */
this.running = false;

/**
 * Initialize the test suite.
 */
this.init = function () {
  elems.output = document.createElement('div');
  elems.output.id = 'output';
  document.body.insertBefore(elems.output, document.body.firstChild);

  this.counter_conditions = 0;
  this.cond_failed = false;
  this.running = false;

  this.output('Initialized. Ready to run.');
};

/**
 * Output information in the page.
 *
 * <p>This function can be used by tests for outputting information during 
 * testing. It is also used to inform the user on the execution status of each 
 * test being ran.
 *
 * @param {String} str The message to output.
 * @param {String} [className] The class name to use for the message. Used for 
 * styling.
 */
this.output = function (str, className) {
  var p = document.createElement('p');
  if (className) {
    p.className = className;
  }
  p.appendChild(document.createTextNode(str));
  elems.output.appendChild(p);
};

/**
 * Output debug information in the page.
 *
 * <p>This function is only used if the browser does not support console.log().
 *
 * @param {String} str The debug message to output.
 */
this.debug = function (str) {
  this.output(str, 'debug');
};


/*
 * This counts the number of conditions per test. The value is reset to 0 before 
 * running every test.
 */
this.counter_conditions = 0;
this.test_failed = false;

/**
 * Check if a given condition is met.
 *
 * <p>Tests can use this method to check if given conditions are met.
 * <p>The result of the condition check is automatically displayed as output in  
 * the page.
 *
 * @param a Variable A
 * @param b Variable B
 * @param {String} cond The condition to check between the two variables. The 
 * condition can only be: '===', '!===', '==', '!=' (for now, more will be added 
 * as needed).
 *
 * @throws {TypeError} If the <var>cond</var> provided is unrecognized.
 *
 * @returns {Boolean} The result of the check.
 */
this.check_cond = function (a, b, cond) {
  var i = ++this.counter_conditions;

  var res;

  switch (cond) {
    case '===':
      res = a === b;
      break;
    case '!==':
      res = a !== b;
      break;
    case '==':
      res = a == b;
      break;
    case '!=':
      res = a != b;
      break;
    default:
      throw new TypeError('Wrong test condition #' + i + '!');
      return false;
  }

  if (res) {
    this.output('Condition #' + i + ' passed: a ' + cond + ' b.');
  } else {
    this.report_error('Condition #' + i + ' failed: a ' + cond + ' b.');
  }

  return res;
};

/**
 * Check the effects of $.extend on native objects.
 *
 * <p>The parameters are as follows:
 * dest1, src1, name1, dest2, src2, name2, ...
 *
 * <p>The destination object is the object which was extended. The source object 
 * is the object containing the methods to be added to the destination. The name 
 * is simply used for outputting meaningful information in the table.
 * 
 * <p>The table will show the method name and if it's native or not (if it was 
 * added from the source object).
 * 
 * <p>If, by any chance, the destination object does not contain a function with 
 * the same name (if the property has a different type), then this case is 
 * considered a failure.
 */
this.check_native = function () {
  var n = arguments.length, arr = arguments;
  if (n < 3) {
    throw new Error("Wrong function call. Insufficient arguments.");
  }

  var table = document.createElement('table'),
      tr = document.createElement('tr'),
      th_method = document.createElement("th"),
      th_native = document.createElement("th"),
      td_method, td_native;

  th_method.scope = 'col';
  th_native.scope = 'col';

  th_method.appendChild(document.createTextNode('Method'));
  th_native.appendChild(document.createTextNode('Native'));

  tr.appendChild(th_method);
  tr.appendChild(th_native);
  table.appendChild(tr);

  var i = 0, src, dest, obj_name, method, y, mname, is_native, str;
  while (i < n) {
    dest = arr[i++];
    src = arr[i++];
    obj_name = arr[i++];

    for (method in src) {
      if (typeof src[method] != 'function') {
        continue;
      }

      tr = document.createElement('tr');
      td_method = document.createElement('td');
      td_native = document.createElement('td');

      mname = obj_name + '.' + method;

      if (dest[method] === src[method]) {
        is_native = 'no';
      } else if (typeof dest[method] == 'function') {
        str = dest[method].toString();
        if (str.indexOf('native') > -1) {
          is_native = 'yes';
          td_native.className = 'success';
        } else {
          is_native = 'different non-native method';
          td_native.className = 'failed';
          this.test_failed = true;
        }
      } else {
        is_native = typeof dest[method];
        td_native.className = 'failed';
        this.test_failed = true;
      }

      td_method.appendChild(document.createTextNode(mname));
      td_native.appendChild(document.createTextNode(is_native));
      tr.appendChild(td_method);
      tr.appendChild(td_native);
      table.appendChild(tr);
    }
  }

  elems.output.appendChild(table);
};

/**
 * Report a non-fatal test error.
 *
 * @param [String} msg The error message.
 */
this.report_error = function (msg) {
  this.test_failed = true;
  this.output(msg);
};

/**
 * Start running the tests.
 *
 * <p>For testing libmacrame we do not use the library itself, since we cannot 
 * rely on what we test for bugs. Each test page includes needed parts of the 
 * library, and tests the provided functionality on an individual basis.
 */
this.start = function () {
  var failed = false, name, fn_test, fn_failed = false, i = 0,
      time_start = (new Date()).getTime();

  for (name in tests) {
    fn_test = tests[name];
    this.counter_conditions = 0;
    this.test_failed = false;
    this.running = name;

    this.output('Running test ' + name + '...', 'test_start');

    // Each test must not throw and must not return anything, to be considered 
    // a successful run.
    try {
      fn_failed = fn_test();
    } catch (err) {
      this.test_failed = true;
      this.output(err);
      console.log(err);
    }

    if (fn_failed) {
      this.test_failed = true;
      this.output(fn_failed);
    }

    if (this.test_failed) {
      this.output('Test failed.', 'test_failed');
    } else {
      this.output('No errors.', 'test_success');
    }

    i++;
  }

  var time_end = (new Date()).getTime();
  var time_run = time_end - time_start;

  this.output('Done. Ran ' + i + ' tests in ' + time_run + ' ms.');

  this.running = false;
  this.counter_conditions = 0;
  this.test_failed = false;
};
})(); // end of the $t object

var addEv = window.addEventListener;

// Minimal MSIE support.
if (window.attachEvent && !window.addEventListener) {
  addEv = function (ev, fn) {
    var _self = window,
        listener = function () {
          return fn.call(_self, window.event);
        };

    window.attachEvent('on' + ev, listener);
  };
}

addEv('load', function () {
  $t.init();
  $t.start();
}, false);

/**
 * @namespace Holds the tests needed to be run. Each page must add new methods 
 * to this object to be executed on page load.
 */
var tests = {};

